(function (fruit, util, ui, dom, engine) {
    var elements = {};
    var typeMap = {};
    if (fruit.DEBUG) {
        fruit.$ = elements;
    }

    engine.registerEventModule('sender', {
        active: true,
        process: function (payload, item, meta) {
            payload.eventArgs.unshift(meta);
        }
    });

    /**
     * @class fruit.ui.Element
     * Wrapper class for dom elements.
     * @type {*}
     */
    var Element = fruit.define('fruit.ui.Element', fruit.ObservableObject, {
        events: ['itemGenerated'],
        statics: {
            register: function (type, cls) {
                typeMap[type] = cls;
            },
            resolve: function (el, owner) {
                if (!el) {
                    return EmptyElement;
                }
                else {
                    var id = dom.identity(el);
                    var element = elements[id];
                    var cls = null;
                    if (!element) {
                        var typeValue = el.getAttribute('data-fruit-type');
                        if (typeValue) {
                            var array = typeValue.split(':');
                            var typeId = array[0];
                            cls = fruit.getClassById(typeId);
                        }
//                        var cls = util.getByPath();
                        if (cls) {
                            element = elements[id] = cls.create("_init", [el]);
                        }
                        else {
                            element = elements[id] = new Element(el);
                        }
                    }

                    if (owner) {
                        if (!element.owner()) {
                            element.owner(owner);
                        }

                    }

                    return element;
                }
            },
            registerInstance: function (comp) {
                var id = dom.identity(comp._element);
                elements[id] = comp;
            }
        },
        properties: {
            /**
             * Owner of current element.
             */
            owner: {
                observable: true
            },
            /**
             * Data model of current element.
             */
            model: {
                getter: function () {
                    return this._model || this._inheritedModel;
                },
                setter: function (value) {
                    if (this._model !== value) {
                        this._model = value;
                        this.notifyPropertyChanged('model');
                        this.children(function (child) {
                            child._inheritModel(value);
                        });
                    }
                }
            },
            /**
             * Style object of current element.
             */
            style: {
                getter: function () {
                    return this._style;
                },
                setter: function (value) {
                    if (typeof value == 'string') {
                        this.setAttribute('style', value);
                    }
                    else {
                        this.setStyles(value);
                    }
                }
            },
            /**
             * Class object of current element.
             */
            'class': {
                getter: function () {
                    return this._class;
                },
                setter: function (value) {
                    this.setAttribute('class', value);
                }
            },
            /**
             * Get or set whether current element is visible.
             */
            visible: {
                getter: function () {
                    return dom.isVisible(this._element);
                },
                setter: function (value) {
                    if (value) {
                        this.removeClass('hidden');
                    }
                    else {
                        this.addClass('hidden');
                    }
                }
            },
            /**
             * Get or set data items for children generation
             */
            items: {
                getter: function () {
                    return this._items;
                },
                setter: function (value) {
                    var items = value;
                    if (items && items.off) {
                        items.off('collectionChanged', this._onCollectionChanged, this);
                    }

                    items = this._items = value;
                    if (items && items.on) {
                        items.on('collectionChanged', this._onCollectionChanged, this);
                    }

                    this._generateItems();
                }
            },
            /**
             * Get or set the template for children generation.
             */
            template: {
                getter: function () {
                    return this._template;
                },
                setter: function (value) {
                    this._template = value;
                    if (value) {
                        var type = value.type;
                        if (type) {
                            var props = value.props;
                            var type = util.isString(type) ? util.getByPath(type) : type;
                            var events = value.events;
                            this._itemClass = fruit.ui.define({
                                view: {
                                    type: type,
                                    props: props,
                                    events: events
                                }
                            });
                        } else {
                            var members;
                            if (value.view) {
                                members = value;
                            }
                            else {
                                members = {
                                    view: value
                                }
                            }

                            this._itemClass = ui.define(members);
                        }
                    }

                    this._generateItems();
                }
            },
            /**
             * Generator for children generation.
             */
            itemGenerator: {
                getter: function () {
                    return this._itemGenerator || function (itemEl, itemModel) {
                        itemEl.model(itemModel);
                        return itemEl;
                    };
                },
                setter: function (value) {
                    this._itemGenerator = value;

                    this._generateItems();
                }
            }
        },
        methods: {
            /**
             * Constructor method
             * @param el
             */
            init: function (el) {
                this._element = el;
                this._style = new CssStyle(el);
                this._class = new CssClass(el);
                this._bindings = {};

                var self = this;
                var tagName = el.tagName;

                if (tagName) {
                    switch (tagName.toLowerCase()) {
                        case 'input':
                        case 'textarea':
                            this.on('CHANGE', function (sender, event) {
                                switch (event.target.type) {
                                    case 'checkbox':
                                    case 'radio':
                                        self.notifyPropertyChanged('checked');
                                        break;
                                    default:
                                        self.notifyPropertyChanged('value');
                                        break;
                                }
                            }, this);
                            break;
                        case 'select':
                            this.on('CHANGE', function (sender, event) {
                                self.notifyPropertyChanged('selectedIndex');
                                self.notifyPropertyChanged('value');
                            }, this);
                            break;
                    }
                }
            },
            /**
             * Get specified property value.
             * @param name
             * @returns {*}
             */
            get: function (name) {
                if (this[name]) {
                    return this[name].call(this);
                }
                else if (name == 'class') {
                    return this._class;
                }
                else if (name == 'style') {
                    return this._style
                }
                else {
                    return this.getAttribute(name);
                }
            },
            /**
             * Set specified property value.
             * @param name
             * @param value
             * @returns {*}
             */
            set: function (name, value) {
                if (name == 'content') {
                    return this.setContent(value);
                }
                else if (this[name]) {
                    return this[name](value);
                }
                else {
                    if (value !== this.get(name)) {
                        this.notifyPropertyChanged(name, value);
                        return this.setAttribute(name, value);
                    }
                }
            },
            /**
             * Try invoking native method of element.
             * @param name
             */
            invoke: function (name) {
                var el = this._element;
                if (typeof el[name] == 'function') {
                    el[name]();
                }
            },
            /**
             * Get existing binding object for specified property.
             * @param prop
             * @returns {*}
             */
            getBinding: function (prop) {
                return this._bindings[prop];
            },
            /**
             * Set binding for specified property.
             * @param prop
             * @param path
             * @param config
             */
            setBinding: function (prop, path, config) {
                config = config || {};
                var target = config.target;
                var source = config.source;
                var sourcePath = path;
                var twoWay = config.twoWay;

                var binding = this._bindings[prop];
                if (binding) {
                    binding.destroy();
                }

                if (sourcePath[0] == '#') {
                    sourcePath = 'owner.' + sourcePath.slice(1);
                }
                else if (sourcePath[0] == '~') {
                    sourcePath = sourcePath.slice(1);
                    source = window;
                }
                else if (sourcePath == '@value') {
                    sourcePath = 'model';
                    twoWay = false;
                }
                else {
                    sourcePath = sourcePath ? ('model.' + sourcePath) : 'model';
                }

                this._bindings[prop] = new fruit.Binding({
                    target: target || this,
                    targetPath: prop,
                    source: source || this,
                    sourcePath: sourcePath,
                    formatter: config.formatter,
                    twoWay: twoWay
                });
            },
            /**
             * Clear binding for specified property.
             * @param prop
             */
            clearBinding: function (prop) {
                var binding = this._bindings[prop];
                if (binding) {
                    binding.destroy();
                    this._bindings[prop] = null;
                }
            },
            /**
             * Check whether current object has event.
             * @param name
             * @returns {*}
             */
            can: function (name) {
                return this.inherited(name) || engine.hasEvent(this._element, name);
            },
            /**
             * Add event handler.
             * @param name
             * @param handler
             * @param context
             */
            on: function (name, handler, context) {
                var event = this['on' + name];
                if (event) {
                    event.addListener(this, handler, context);
                }
                else {
                    engine.on(this._element, name, handler, context || this, {sender: this});
                }
            },
            /**
             * Remove event handler.
             * @param name
             * @param handler
             * @param context
             */
            off: function (name, handler, context) {
                var event = this['on' + name];
                if (event) {
                    if (handler === undefined) {
                        event.clearListeners(this);
                    }
                    else {
                        event.removeListener(this, handler, context);
                    }
                }
                else {
                    engine.off(this._element, name, handler, context || this);
                }
            },
            /**
             * Add single event handler.
             * @param name
             * @param handler
             * @param context
             */
            upon: function (name, handler, context) {
                var event = this['on' + name];
                if (event) {
                    event.call(this, handler, context);
                }
                else {
                    engine.upon(this._element, name, handler, context || this, {sender: this});
                }
            },
            /**
             * Clone dom node.
             * @param deep
             * @returns {*}
             */
            clone: function (deep) {
                return Element.resolve(dom.cloneNode(this._element, deep));
            },
            /**
             * Show current element.
             */
            show: function () {
                dom.show(this._element);
            },
            /**
             * Hide current element.
             */
            hide: function () {
                dom.hide(this._element);
            },
            /**
             * Show or hide current element.
             */
            toggle: function () {
                dom.toggle(this._element);
            },
            /**
             * Add specified class to current element.
             * @param cls
             */
            addClass: function (cls) {
                dom.addClass(this._element, cls);
            },
            /**
             * Remove specified class from current element.
             * @param cls
             */
            removeClass: function (cls) {
                dom.removeClass(this._element, cls);
            },
            /**
             * Toggle specified class to current element
             * @param cls
             */
            toggleClass: function (cls) {
                dom.toggleClass(this._element, cls);
            },
            /**
             * Check whether current element has specified class.
             * @param cls
             * @returns {*}
             */
            hasClass: function (cls) {
                return dom.hasClass(this._element, cls);
            },
            /**
             * Get style from current element.
             * @param name
             * @returns {*}
             */
            getStyle: function (name) {
                return this._style.get(name);
            },
            /**
             * Set style for current element.
             * @param name
             * @param value
             */
            setStyle: function (name, value) {
                this._style.set(name, value);
            },
            /**
             * Batch set styles for current element.
             * @param inStyleObj
             */
            setStyles: function (inStyleObj) {
                dom.setStyles(this._element, inStyleObj);
            },
            /**
             * Set focus to current element.
             */
            focus: function () {
                dom.focus(this._element);
            },
            /**
             * Remove focus from current element.
             */
            blur: function () {
                dom.blur(this._element);
            },
            /**
             * Check whether current element is hidden.
             * @returns {*}
             */
            isHidden: function () {
                return dom.isHidden(this._element);
            },
            /**
             * Get the bound of current element.
             * @returns {*}
             */
            getBound: function () {
                return fruit.position.getBound(this._element);
            },
            /**
             * Get specified attribute value for current element.
             * @param name
             * @returns {*}
             */
            getAttribute: function (name) {
                return dom.getAttribute(this._element, name);
            },
            /**
             * Set specified attribute for current element.
             * @param name
             * @param value
             */
            setAttribute: function (name, value) {
                dom.setAttribute(this._element, name, value);
            },
            /**
             * Remove specified attribute from current element.
             * @param name
             */
            removeAttribute: function (name) {
                dom.removeAttribute(this._element, name);
            },
            /**
             * Get the parent node of current element.
             * @returns {*}
             */
            parent: function () {
                return Element.resolve(dom.parent(this._element));
            },
            parents: function (fn) {
                var parent = this.parent();
                while (parent) {
                    if (fn.call(this, parent)) {
                        return parent
                        break;
                    }
                    parent = parent.parent();
                }
                return null;
            },
            /**
             * Get the first child element of current element.
             * @returns {*}
             */
            firstChild: function () {
                return Element.resolve(dom.firstChild(this._element), this.owner());
            },
            /**
             * Get the last child element of current element.
             * @returns {*}
             */
            lastChild: function () {
                return Element.resolve(dom.lastChild(this._element), this.owner());
            },
            /**
             * Get previous sibling element of current element.
             * @returns {*}
             */
            previousSibling: function () {
                return Element.resolve(dom.prevSibling(this._element), this.owner());
            },
            /**
             * Get next sibling element of current element.
             * @returns {*}
             */
            nextSibling: function () {
                return Element.resolve(dom.nextSibling(this._element), this.owner());
            },
            /**
             * Get child elements.
             * @param fn
             * @returns {Array}
             */
            children: function (fn) {
                if (this._element) {
                    var elements = dom.children(this._element) || [];
                    var children = [], child;
                    for (var i = 0, length = elements.length; i < length; i++) {
                        child = Element.resolve(elements[i]);
                        children.push(child);
                        fn && fn(child, i);
                    }
                    return children;
                }
            },
            /**
             * Check whether current element contains specified element.
             * @param inTarget
             * @param isSelf
             * @returns {*}
             */
            contains: function (inTarget, isSelf) {
                return dom.contains(this._element, inTarget._element, isSelf);
            },
            /**
             * Get child element by index.
             * @param index
             * @returns {*}
             */
            childAt: function (index) {
                return Element.resolve(dom.childAt(this._element, index), this.owner());
            },
            /**
             * Get the index of current element.
             * @returns {*}
             */
            index: function () {
                return dom.index(this._element);
            },
            /**
             * Append child element to current element.
             * @param child
             */
            appendChild: function (child) {
                dom.appendChild(this._element, child._element);
                child.owner(this.owner());
            },
            /**
             * Append current element as the child to specified element.
             * @param container
             */
            appendTo: function (container) {
                var target = container || Element.resolve(document.body);
                dom.appendChild(target._element, this._element);
                this.owner(target.owner());
            },
            /**
             * Insert child element into current element specified by index.
             * @param child
             * @param index
             */
            insertAt: function (child, index) {
                dom.insertAt(this._element, child._element, index);
                child.owner(this.owner());
            },
            /**
             * Remove specified child element from current element.
             * @param child
             */
            removeChild: function (child) {
                child.destroy();
            },
            /**
             * Remove child element from current element specified by index.
             * @param index
             */
            removeAt: function (index) {
                this.childAt(index).destroy();
            },
            /**
             * Remove all child elements.
             */
            empty: function () {
                var children = this.children();
                for (var i = 0, length = children.length; i < length; i++) {
                    children[i].destroy();
                }
                this._element.innerHTML = "";
            },
            setContent: function (content) {
                this.empty();
                if (content) {
                    if (util.isObject(content)) {
                        var comp = ui.viewEngine.create(content);
                        comp._inheritModel(this.model());
                        this.appendChild(comp);
                    }
                    else {
                        this.set('html', content);
                    }
                }
            },
            /**
             * Destroy current element.
             */
            destroy: function () {
                this.model(null);
                this.owner(null);
                dom.destroy(this._element);
                util.each(this._bindings, function (binding) {
                    if (binding) {
                        binding.destroy();
                    }
                });
                if (elements[this.$id]) {
                    elements[this.$id] = null;
                }
                this.inherited();
            },
            _inheritModel: function (model) {
                if (this._model === undefined && this._inheritedModel !== model) {
                    var modelType = this.model.config.type;
                    if (modelType) {
                        model = modelType.$type ? new modelType(model) : modelType(model);
                    }
                    this._inheritedModel = model;
                    this.notifyPropertyChanged('model');
                    this.children(function (child) {
                        child._inheritModel(model);
                    });
                }
            },
            _onCollectionChanged: function (sender, event) {
                var action = event.action;
                if (action == 'add') {
                    this._generateItems();
                }
                else if (action == 'remove') {
                    this._generateItems();
                }
                else if (action == 'reset') {
                    this._generateItems();
                }
            },
            _generateItems: function () {
                var items = this.items();
                var itemGenerator = this.itemGenerator();
                var itemClass = this._itemClass;
                this.empty();
                if (items && itemClass && itemGenerator) {
                    util.forEach(items, function (data, i) {
                        var item = itemClass.create("_defaultInit", [null, this.owner()]);
                        data['__bindingEl' + this.$id] = item;
                        item = itemGenerator.call(this, item, data, i);
                        this.appendChild(item);
                        this.fire('itemGenerated', {
                            item: item,
                            model: data
                        });
                    }, this);
                }
            }
        }
    });

    var CssStyle = fruit.define('fruit.ui.CssStyle', {
        methods: {
            init: function (el) {
                if (el) {
                    this._bindings = {};
                    this._element = el;
                    this._style = el.style;
                }
            },
            get: function (name) {
                return dom.getStyle(this._element, name);
            },
            set: function (name, value) {
                dom.setStyle(this._element, name, value);
            },
            getBinding: function (prop) {
                return this._bindings[prop];
            },
            setBinding: function (prop, path, config) {
                config = config || {};
                var target = config.target;
                var source = config.source;
                var sourcePath = path;

                var binding = this._bindings[prop];
                if (binding) {
                    binding.destroy();
                }

                if (sourcePath[0] == '#') {
                    sourcePath = 'owner.' + sourcePath.slice(1);
                }
                else if (sourcePath[0] == '~') {
                    sourcePath = sourcePath.slice(1);
                    source = window;
                }
                else if (sourcePath == '@value') {
                    sourcePath = 'model';
                }
                else {
                    sourcePath = sourcePath ? ('model.' + sourcePath) : 'model';
                }

                this._bindings[prop] = new fruit.Binding({
                    target: target || this,
                    targetPath: prop,
                    source: source || this,
                    sourcePath: sourcePath,
                    formatter: config.formatter
                });
            },
            clearBinding: function (prop) {
                var binding = this._bindings[prop];
                if (binding) {
                    binding.destroy();
                    this._bindings[prop] = null;
                }
            }
        }
    });
    var CssClass = fruit.define('fruit.ui.CssClass', {
        methods: {
            init: function (el) {
                if (el) {
                    this._bindings = {};
                    this._element = el;
                    this._style = el.style;
                    this._metadata = {};
                }
            },
            setMeta: function (meta) {
                var metadata = this._metadata;
                util.each(meta, function (value, key) {
                    metadata[key] = value;
                });
            },
            get: function (name) {
                return this._metadata[name] || false;
            },
            set: function (name, value) {
                var metadata = this._metadata, classNames = [];
                metadata[name] = value;
                util.each(metadata, function (value, key) {
                    if (value === true) {
                        classNames.push(key);
                    }
                    else if (value === false) {

                    }
                    else {
                        classNames.push(value);
                    }
                });

                dom.setAttribute(this._element, 'class', classNames.join(' '));
            },
            getBinding: function (prop) {
                return this._bindings[prop];
            },
            setBinding: function (prop, path, config) {
                config = config || {};
                var target = config.target;
                var source = config.source;
                var sourcePath = path;

                var binding = this._bindings[prop];
                if (binding) {
                    binding.destroy();
                }

                if (sourcePath[0] == '#') {
                    sourcePath = 'owner.' + sourcePath.slice(1);
                }
                else if (sourcePath[0] == '~') {
                    sourcePath = sourcePath.slice(1);
                    source = window;
                }
                else if (sourcePath == '@value') {
                    sourcePath = 'model';
                }
                else {
                    sourcePath = sourcePath ? ('model.' + sourcePath) : 'model';
                }

                this._bindings[prop] = new fruit.Binding({
                    target: target || this,
                    targetPath: prop,
                    source: source || this,
                    sourcePath: sourcePath,
                    formatter: config.formatter
                });
            },
            clearBinding: function (prop) {
                var binding = this._bindings[prop];
                if (binding) {
                    binding.destroy();
                    this._bindings[prop] = null;
                }
            }
        }
    });

    var EmptyElement = fruit.define('fruit.ui.EmptyElement', Element, {
        singleton: true,
        methods: {
            init: function () {
            },
            clone: function (deep) {
            },
            addClass: function () {
            },
            hasClass: function () {
            },
            removeClass: function () {
            },
            getStyle: function (name) {
            },
            setStyle: function (name, value) {
            },
            getAttribute: function (name) {
            },
            setAttribute: function (name, value) {
            },
            parent: function () {
            },
            firstChild: function () {
            },
            lastChild: function () {
            },
            previousSibling: function () {
            },
            nextSibling: function () {
            },
            children: function () {
            },
            childAt: function (index) {
            },
            index: function () {
                return -1;
            },
            appendChild: function (el) {
            },
            appendTo: function (el) {
            },
            insertChild: function (el, index) {
            },
            insertTo: function (el, index) {
            },
            empty: function () {
            },
            onDomEvent: function (name, handler) {
            },
            offDomEvent: function (name, handler) {
            },
            setBinding: function () {
            }
        }
    });
})(fruit, fruit.util, fruit.ui, fruit.dom, fruit.eventEngine);
